/*******************************************************************************
 * Copyright (c) 2003 Boris Pruessmann and others.
 * All rights reserved.   This program and the accompanying materials
 * are made available under the terms of the Common Public License v0.5
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v05.html
 * 
 * Contributors:
 *    Boris Pruessmann - Initial implementation.
 ******************************************************************************/
package net.sourceforge.perforce.ui.views;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import net.sourceforge.perforce.api.IError;
import net.sourceforge.perforce.api.IPerforceCommand;
import net.sourceforge.perforce.core.IClientUserListener;
import net.sourceforge.perforce.core.PerforceCorePlugin;
import net.sourceforge.perforce.ui.PerforceUIPlugin;
import net.sourceforge.perforce.ui.internal.ConsoleDocument;

import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.TextViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.LineStyleEvent;
import org.eclipse.swt.custom.LineStyleListener;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.part.ViewPart;

/**
  * @version $Revision: 250 $
  * @author  <a href="mailto:bpruessmann@users.sourceforge.net">Boris Pruessmann</a>
  */
public class Console extends ViewPart
{
	// ---------------------------------------------------------------------------- Public Constants
	
	/** Unique identifier of the Console view. */
	public static final String ID = PerforceUIPlugin.ID + ".views.Console";            //$NON-NLS-1$

	// --------------------------------------------------------------------------- Private Constants
	
	/** Time format for console output. */
	private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss.SSS");

	// -------------------------------------------------------------------- Private Static Variables

	/** Stores the document that holds the console text. */
	private static ConsoleDocument document;
	/** List of console instances. */
	private static List instances;
	/** Stores the client user listener. */
	private static IClientUserListener clientUserListener;

	// --------------------------------------------------------------------------- Private Variables
	
	/** Stores the text viewer instance. */
	private TextViewer viewer;
	/** Stores the document listener. */
	private IDocumentListener documentListener;
	
	/** Color for commands. */
	private Color commandColor;
	/** Color for message. */
	private Color messageColor;
	/** Color for errors. */
	private Color errorColor;

	/** Action to clear the console's output. */
	private Action clearOutputAction;
	
	// ----------------------------------------------------------------------- Public Static Methods
	
	/** Called upon startup. */
	public static void startup()
	{
		document = new ConsoleDocument();
		instances = new ArrayList();
		
		clientUserListener = new ClientUserListener();
		PerforceCorePlugin.getPlugin().addClientUserListener(clientUserListener);
	}
	
	/** Called upon shutdown. */
	public static void shutdown()
	{
		PerforceCorePlugin.getPlugin().removeClientUserListener(clientUserListener);
		clientUserListener = null;
		document = null;
		instances = null;
	}
	
	// ------------------------------------------------------------------------------ Public Methods
	
	/** @see IWorkbenchPart#createPartControl(Composite) */
	public void createPartControl(Composite parent) 
	{
		Display display = Display.getCurrent();
		if (display == null) 
		{
			display = Display.getDefault();
		}
		
		commandColor = createColor(display, new RGB(0x00, 0x00, 0x00));
		messageColor = createColor(display, new RGB(0x00, 0x00, 0x00));
		errorColor = createColor(display, new RGB(0xff, 0x00, 0x00));
		
		Composite composite = new Composite(parent, SWT.NULL);
		GridLayout layout = new GridLayout();
		layout.marginHeight = 0;
		layout.marginWidth = 0;
		composite.setLayout(layout);
		composite.setLayoutData(
			new GridData(GridData.VERTICAL_ALIGN_FILL | GridData.HORIZONTAL_ALIGN_FILL));

		viewer = new TextViewer(composite, SWT.V_SCROLL | SWT.H_SCROLL);
		GridData data = new GridData(GridData.FILL_BOTH);
		viewer.getControl().setLayoutData(data);
		viewer.setEditable(false);
		viewer.setDocument(document);
		// TODO: viewer.getTextWidget().setFont(consoleFont);
			
		// add a line styler for colouring lines according to their type
		viewer.getTextWidget().addLineStyleListener(new LineStyleListener() 
		{
			public void lineGetStyle(LineStyleEvent event) 
			{
				StyleRange style = 
					new StyleRange(
						event.lineOffset, 
						event.lineText.length(),
						getConsoleLineColor(event.lineOffset), 
						null);
						
				event.styles = new StyleRange[] { style };
			}
		});			
			
		documentListener = new IDocumentListener() 
		{
			public void documentAboutToBeChanged(DocumentEvent event) 
			{
			}
			 
			public void documentChanged(DocumentEvent event)
			{
				if (viewer == null) 
				{
					return;
				} 
				 					 
				StyledText styledText = viewer.getTextWidget();
				if (!styledText.isDisposed()) 
				{
					styledText.setTopIndex(styledText.getLineCount());
				}
			}
		};
		document.addDocumentListener(documentListener);
	
		contributeActions();
		
		instances.add(this);
	}

	/** @see org.eclipse.ui.IWorkbenchPart#setFocus() */
	public void setFocus() 
	{
		viewer.getTextWidget().setFocus();
	}
	
	/** @see org.eclipse.ui.IWorkbenchPart#dispose() */
	public void dispose() 
	{
		if (documentListener != null)
		{
			document.removeDocumentListener(documentListener);
			documentListener = null;
		}
		
		instances.remove(this);		
		super.dispose();
		
		if (commandColor != null)
		{
			commandColor.dispose();
			commandColor = null;
		}
		
		if (messageColor != null)
		{
			messageColor.dispose();
			messageColor = null;
		}
		
		if (errorColor != null)
		{
			errorColor.dispose();
			errorColor = null;
		}
	}

	// ----------------------------------------------------------------------------- Private Methods

	private void contributeActions() 
	{
		final ImageDescriptor imageDescrptor = 
			PerforceUIPlugin.getPlugin().getImageDescriptor(PerforceUIPlugin.IMG_CLEAR);
		clearOutputAction = new Action("Clear Console", imageDescrptor) 
		{
			public void run() 
			{
				document.clear();
			}
		};	
		
		IToolBarManager tbm = getViewSite().getActionBars().getToolBarManager();
		tbm.add(clearOutputAction);
		tbm.update(false);
	}
	
	/** Returns the color for the line containing the specified offset. */
	private Color getConsoleLineColor(int offset) 
	{
		switch (document.getLineType(offset)) {
			case ConsoleDocument.COMMAND:
			case ConsoleDocument.DELIMITER:
				return commandColor;
				
			case ConsoleDocument.MESSAGE:
				return messageColor;
				
			case ConsoleDocument.ERROR:
				return errorColor;
				
			default:
				throw new IllegalStateException();
		}
	}

	/** Returns a color instance based on data from a preference field. */
	private Color createColor(Display display, RGB rgb) 
	{
		return new Color(display, rgb);
	}
			
	/** Appends a line to the console if any views are open. */
	private static void appendConsoleLine(final int type, final String line) 
	{
		Display display = Display.getCurrent();
		if (display == null) 
		{
			display = Display.getDefault();
		}
		
		display.asyncExec(new Runnable() 
		{
			public void run() 
			{
				if (document == null)
				{
					return;
				}
				 
				document.appendConsoleLine(type, line, instances.isEmpty());
			}
		});
	}
	
	// ---------------------------------------------------------------------- Private Nested Classes
	
	/** Implementation of IClientUserListener. */
	private static class ClientUserListener implements IClientUserListener 
	{
		// ----------------------------------------------------------------------- Private Variables
		
		/** Set containing the names of commands for which we want to display detail information. */
		private Set detailSet = new HashSet();
		/** Set containing commands to exclude. */
		private Set excludeSet = new HashSet();
		/** Stores the starting times of commands. */
		private Map startTimeMap = new Hashtable(); 
		
		// --------------------------------------------------------------------- Public Constructors
		
		/**  Constructor for ClientUserListener. */
		public ClientUserListener()
		{
			detailSet.add("add");                                                      //$NON-NLS-1$
			detailSet.add("delete");                                                   //$NON-NLS-1$
			detailSet.add("edit");                                                     //$NON-NLS-1$
			detailSet.add("revert");                                                   //$NON-NLS-1$
			detailSet.add("submit");                                                   //$NON-NLS-1$
			detailSet.add("sync");                                                     //$NON-NLS-1$
			
			excludeSet.add("where");                                                   //$NON-NLS-1$
		}
		
		// -------------------------------------------------------------------------- Public Methods
		
		/** @see IClientUserListener#run(IPerforceCommand, String[]) */
		public void run(IPerforceCommand command, String[] arguments) 
		{
			if (isExcluded(command))
			{
				return;
			}
			
			startTimeMap.put(command, new Long(System.currentTimeMillis()));
			
			StringBuffer buffer = new StringBuffer();
			buffer.append(TIME_FORMAT.format(new Date())).append("    ");              //$NON-NLS-1$
			buffer.append("Executing p4 ");                                            //$NON-NLS-1$
			buffer.append(command.getCommand()).append(" ");                           //$NON-NLS-1$
			for (int i = 0; i < arguments.length; i++)
			{
				buffer.append(arguments[i]).append(" ");                               //$NON-NLS-1$
			}
			
			appendConsoleLine(ConsoleDocument.COMMAND, buffer.toString());
		}

		/** @see IClientUserListener#handleError(IPerforceCommand, IError) */
		public void handleError(IPerforceCommand command, IError error) 
		{
			if (isExcluded(command))
			{
				return;
			}

			int type = ConsoleDocument.ERROR;
			switch (error.getSeverity())
			{
				case IError.E_INFO:
				case IError.E_WARN:
					type = ConsoleDocument.MESSAGE;
					break;
									
				default:
					type = ConsoleDocument.ERROR;
					break;
			}
			
			final Date timestamp = new Date();
			StringTokenizer tokenizer = new StringTokenizer(error.format(IError.EF_PLAIN), "\n");
			while (tokenizer.hasMoreElements())
			{	
				final String message = (String) tokenizer.nextElement();					
				
				StringBuffer buffer = new StringBuffer(); 				
				buffer.append(TIME_FORMAT.format(timestamp)).append("    ");          //$NON-NLS-1$
				
				buffer.append("    " + message);                                       //$NON-NLS-1$
				
				
				appendConsoleLine(type, buffer.toString());
			}
		}

		/** @see IClientUserListener#outputError(String) */
		public void outputError(IPerforceCommand command, String errorString) 
		{
			if (isExcluded(command))
			{
				return;
			}
						
			StringBuffer buffer = new StringBuffer(); 
			buffer.append(TIME_FORMAT.format(new Date())).append("    ");              //$NON-NLS-1$
			buffer.append("    " + errorString);                                       //$NON-NLS-1$
			
			appendConsoleLine(ConsoleDocument.ERROR, buffer.toString());
		}

		/** @see IClientUserListener#outputInfo(char, String) */
		public void outputInfo(IPerforceCommand command, char level, String infoString) 
		{
			if (isExcluded(command))
			{
				return;
			}
			
			if (detailSet.contains(command.getCommand()))
			{
				StringBuffer buffer = new StringBuffer(); 
				buffer.append(TIME_FORMAT.format(new Date())).append("    ");          //$NON-NLS-1$
				buffer.append("    " + infoString);                                    //$NON-NLS-1$
				
				appendConsoleLine(ConsoleDocument.MESSAGE, buffer.toString());
			}
		}
		
		/** @see IClientUserListener#finished(IPerforceCommand) */
		public void finished(IPerforceCommand command) 
		{
			if (isExcluded(command))
			{
				return;
			}
			
			Long startTime = (Long) startTimeMap.remove(command);
			long duration = System.currentTimeMillis() - startTime.longValue();
			
			StringBuffer buffer = new StringBuffer(); 
			buffer.append(TIME_FORMAT.format(new Date())).append("    ");              //$NON-NLS-1$
			buffer.append("    Took " + duration + " milliseconds.");    //$NON-NLS-1$ //$NON-NLS-1$
			
			appendConsoleLine(ConsoleDocument.MESSAGE, buffer.toString());
			appendConsoleLine(ConsoleDocument.DELIMITER, "");                          //$NON-NLS-1$
		}
		
		/** Checks whether the given command is excluded. */
		private boolean isExcluded(IPerforceCommand command) 
		{
			return excludeSet.contains(command.getCommand());
		}
	}
}
